/*
 * Copyright 2010 Google Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.websocket;

import org.eclipse.jetty.util.Utf8StringBuilder;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.Writer;
import java.util.Set;

/**
 * Implements framing for WebSocket v76.
 */
public class V76WebSocket implements WebSocket {

  private static final byte FRAME_BINARY = (byte) 0x80;
  private static final byte FRAME_TEXT = (byte) 0x00;
  private static final int FRAME_TEXT_SENTINEL = 0xFF;

  /**
   * Return true if the supplied opcode indicates binary framing.
   * 
   * @param opcode
   * @return true if opcode is binary
   */
  private static boolean isBinaryOpcode(int opcode) {
    return (opcode & 0x80) != 0;
  }
  
  private BufferedInputStream bufIn;

  private BufferedOutputStream bufOut;

  /**
   * Create a WebSocket connection on an already-established connection.  The
   * caller is responsible for completing the connection establishment phase and
   * any extension negotiation before creating this object.
   * 
   * @param in
   * @param out
   */
  public V76WebSocket(InputStream in, OutputStream out) {
    bufIn = in instanceof BufferedInputStream ? (BufferedInputStream) in
        : new BufferedInputStream(in);
    bufOut = out instanceof BufferedOutputStream ? (BufferedOutputStream) out
        : new BufferedOutputStream(out);
  }

  public void close() throws IOException {
    bufOut.close();
  }

  public boolean readOneFrame(Callback callback) throws IOException {
    bufOut.flush();
    int opcode = Util.readByte(bufIn);
    if (isBinaryOpcode(opcode)) {
      long length = Util.readV76Length(bufIn);
      byte[] buf = new byte[(int) length];
      Util.readFully(buf, bufIn);
      callback.onBinaryMessage(buf, null);
    } else {
      Utf8StringBuilder buf = new Utf8StringBuilder();
      int b;
      while ((b = Util.readByte(bufIn)) != FRAME_TEXT_SENTINEL) {
        buf.append((byte) b);
      }
      callback.onTextMessage(buf.toString(), null);
    }
    return false;
  }

  public void readOneMessage(Callback callback) throws IOException {
    readOneFrame(callback);
  }

  public OutputStream sendBinaryMessage(Set<ExtensionData> metadata)
      throws IOException {
    return sendStream(FRAME_BINARY, metadata);
  }

  public void sendBinaryMessage(Set<ExtensionData> metadata, byte[] message)
      throws IOException {
    sendFrame(false, FRAME_BINARY, metadata, message);
  }

  public void sendFrame(
      boolean more, int opcode, Set<ExtensionData> metadata, byte[] payload)
      throws IOException {
    if (metadata != null && !metadata.isEmpty()) {
      throw new IOException("V76 doesn't support metadata");
    }
    bufOut.write(opcode);
    if (isBinaryOpcode(opcode)) {
      Util.writeV76Length(bufOut, payload.length);
    }
    bufOut.write(payload);
    if (!isBinaryOpcode(opcode)) {
      bufOut.write(FRAME_TEXT_SENTINEL);
    }
  }

  public OutputStream sendStream(final int opcode,
      final Set<ExtensionData> metadata) throws IOException {
    if (metadata != null && !metadata.isEmpty()) {
      throw new IOException("V76 doesn't support metadata");
    }
    return new ByteArrayOutputStream() {
      @Override
      public void close() throws IOException {
        super.close();
        sendFrame(false, opcode, metadata, toByteArray());
      }
    };
  }

  public PrintWriter sendTextMessage(final Set<ExtensionData> metadata)
      throws IOException {
    if (metadata != null && !metadata.isEmpty()) {
      throw new IOException("V76 doesn't support metadata");
    }
    return new PrintWriter(new Writer() {
      private final StringBuilder buf = new StringBuilder();

      @Override
      public void close() throws IOException {
        sendTextMessage(metadata, buf.toString());
      }

      @Override
      public void flush() throws IOException {
      }

      @Override
      public void write(char[] cbuf, int off, int len) throws IOException {
        buf.append(cbuf, off, len);
      }
    });
  }

  public void sendTextMessage(Set<ExtensionData> metadata, String message)
      throws IOException {
    sendFrame(false, FRAME_TEXT, metadata, message.getBytes(Util.UTF8));
  }
}
